#!/usr/bin/env python
# -*- coding: utf-8 -*-

from hashlib import md5, sha1
from binascii import a2b_hex,b2a_hex
from struct import pack
from rsa import rsa
import re
import random
import time

from packet_seq import *
from logger import log, fytionException

FetionLoginXML = """<args><device accept-language="default" machine-code="0A0003000000" /><caps value="1FFF" /><events value="7F" /><user-info mobile-no="%s" user-id="%s"><personal version="0" attributes="v4default;alv2-version;alv2-warn;dynamic-version" /><custom-config version="0"/><contact-list version="0" buddy-attributes="v4default" /></user-info><credentials domains="fetion.com.cn;m161.com.cn;www.ikuwa.cn;games.fetion.com.cn;turn.fetion.com.cn;pos.fetion.com.cn;ent.fetion.com.cn;mms.fetion.com.cn"/><presence><basic value="%s" desc="" /><extendeds /></presence></args>"""

class Processor(object):
    
    _I = None
    I = 0
    _Q = 0    
    _sequencer = None
    _fytion = None
    
    def __init__(self, fytion, sequencer, I = 0):
        self._sequencer = sequencer
        self._fytion = fytion
        if I:
            self._I = I
        else:
            Processor.I += 1
            self._I = Processor.I
    
    def get_headers(self, method):
        return [('F', self._fytion._myconf['fetion_id']), ('I', self._I), ('Q', '%s %s' % (self._Q, method))]
    
    def get_request(self, method, uri, version, headers, body = None):
        if body:
            headers.append(('L', len(body)))
        c = "%s %s %s\r\n" % (method, uri, version)
        for k in headers:
            c += k[0] + ': ' + str(k[1]) + "\r\n"
        c += "\r\n"
        if body:
            c += body
        ### TODO: make sure in HTTP mode, there is trailing SIPP sent..
        return c
        
    def get_response(self, status, text, version, headers, body = None):
        if body:
            headers.append(('L', len(body)))
        c = "%s %d %s\r\n" % (version, status, text)
        for k in headers:
            c += k[0] + ': ' + str(k[1]) + "\r\n"
        c += "\r\n"
        if body:
            c += body
        ### TODO: make sure in HTTP mode, there is trailing SIPP sent..
        return c
    
    def recv_pkt(self, data):
        return 1
        
    @staticmethod
    def get_processor(fytion, seq, data):
        """ Get a new request processor """
        if data['method'] == 'BN':
            return bnotice(fytion, seq, int(data['headers']['I']))
        elif data['method'] == 'M':
            return message_recv(fytion, seq, int(data['headers']['I']))
        elif data['method'] == 'I':
            return invited(fytion, seq, int(data['headers']['I']))
        return None

class register(Processor):
    
    _timeout = 600
    def __init__(self, fytion, sequencer, presence = "0"):
        super(register,self).__init__(fytion, sequencer)
        self._presence = presence
        # 0: init, 1: wait_r1, 2: wait_r2, 3: normal, 4: wait_heartbeat
        self._state = 0
        self.lasttalk = time.time()
        sequencer.register_client(self._I, self)
        
    def reg_round1(self):
        self._Q += 1
        headers = self.get_headers('R')
        headers.append(('CN', ''.join(random.choice("0123456789abcdef") for i in xrange(32))))
        headers.append(('CL', 'type="pc",version="%s"' % self._fytion.FetionVer))
        c = self.get_request('R', self._fytion.Domain, self._fytion.SIPCVer, headers)
        
        self._state = 1
        self._sequencer.dispatch(c)
        
    def reg_round2(self, nonce, key):
        body = FetionLoginXML % (self._fytion._myconf['phone'], self._fytion._myconf['user_id'], self._presence)
        p1 = sha1("fetion.com.cn:"+self._fytion._myconf['passwd']).hexdigest()
        p2 = sha1(pack("i",int(self._fytion._myconf['user_id']))+a2b_hex(p1)).hexdigest()
        padding = ''.join(random.choice("0123456789abcdef") for i in xrange(64))
        plain = nonce+a2b_hex(p2)+a2b_hex(padding)
        rsa_e = key[-6:]
        rsa_n = key[:256]
        response = b2a_hex(rsa(plain, rsa_e, rsa_n, False))

        self._Q += 1
        headers = self.get_headers('R')
        headers.append(('A','Digest algorithm="SHA1-sess-v4",response="%s"' % (response)))
        ### XXX: Do not support captcha yet!
        
        c = self.get_request('R', self._fytion.Domain, self._fytion.SIPCVer, headers, body)
        
        self._state = 2
        self._sequencer.dispatch(c)
    
    def reg_session(self, credential):
        self._Q += 1
        headers = self.get_headers('R')
        headers.append(('A', 'TICKS auth="%s"' % credential))
        headers.append(('K', 'text/plain'))
        c = self.get_request('R', self._fytion.Domain, self._fytion.SIPCVer, headers)
        
        self._state = 1
        self._sequencer.dispatch(c)        
    
    def bye(self):
        self._Q += 1
        headers = self.get_headers('R')
        headers.append(('X', '0'))
        c = self.get_request('R', self._fytion.Domain, self._fytion.SIPCVer, headers)
        
        self._state = 5
        self._sequencer.dispatch(c)
        
    def keepalive(self):
        if self._state == 3 and time.time() - self.lasttalk > self._timeout - 20:
            self._Q += 1
            headers = self.get_headers('R')
            headers.append(('N', 'KeepAlive'))
            c = self.get_request('R', self._fytion.Domain, self._fytion.SIPCVer, headers)
            
            self._state = 4
            self.lasttalk = time.time()
            self._sequencer.dispatch(c)
    
    def recv_pkt(self, data):
        # when return 0, sequencer delete the packet from queue
        #log(data)
        if self._state == 0:
            return 1
            
        ### we dont check Q ....
        if self._state == 1:
            if data['status'] == 401:
                nonce = re.search(r'nonce="([0-9A-F]+)"', data['headers']['W']).group(1)
                key = re.search(r'key="([0-9A-F]+)"', data['headers']['W']).group(1)
                self.reg_round2(nonce, key)
            elif data['status'] == 200:
                self._timeout = int(data['headers']['X'])
                self._fytion.register_fin()
                self._state = 3
            else:
                log("Unexpected response: status=%d text=%s" % (data['status'], data['text']))
                log(data)
        elif self._state == 2:
            if data['status'] == 200:
                self.proc_login_msg(data['body'])
                self._timeout = int(data['headers']['X'])
                self._fytion.register_fin()
                self._state = 3
            else:
                log("Unexpected response: status=%d text=%s" % (data['status'], data['text']))
                log(data)
        elif self._state == 4:
            if data['status'] == 200:
                log("KeepAlive")
                self._timeout = int(data['headers']['X'])
                self._state = 3
            else:
                log("Unexpected response: status=%d text=%s" % (data['status'], data['text']))
                log(data)
        elif self._state == 5:
            if data['status'] == 200:
                log("Shutdown")
            else:
                log("Unexpected response: status=%d text=%s" % (data['status'], data['text']))
                log(data)
            # close connection anyway
            self._sequencer.close()
        self.lasttalk = time.time()

        return 0
    
    def proc_login_msg(self, data):
        matches = re.findall(r'<b i="(\d+)" u="([^"]+)" n="([^"]*)" l="[^"]*" (f="(\d+)" )?r="\d+" o="\d+" p="[^"]*"/>', data)
        contacts = {}
        for match in matches:
            contacts[match[0]] = {
                'user_id': match[0], 
                'uri': match[1], 
                'name': match[2], 
                'phone': '',
                'fetion_id': match[4],
                'sms': {},
                'presence': ''
            }
        self._fytion.contacts = contacts

class bnotice(Processor):

    def __init__(self, fytion, sequencer, I):
        super(bnotice,self).__init__(fytion, sequencer, I)
        sequencer.register_service(self._I, self)
        
    def recv_pkt(self, data):
        if data['method'] != 'BN':
            return 1
        
        if data['headers']['N'] == 'PresenceV4':
            self.proc_presence_msg(data['body'])
        elif data['headers']['N'] == 'registration':
            type = re.search(r'type="([^"]+)"', data['body']).group(1)
            if type == "deregistered":
                self._sequencer.close(fytionException(-300, 'Login from other client'))
            else:
                log("Ignore registration BN: %s" % data['body'])
        else:
            log("Ignore BN: %s" % data['headers']['N'])
        
        self._sequencer.deregister_service(self._I, self)
        return 0
        
    def proc_presence_msg(self, data):
        matches = re.findall(r'<c id="(\d+)"><p v="\d*" (sid="(\d+)" )?su="([^"]+)" m="(\d*)" [^>]*svc="\d*" (n="([^"]*)" )?[^>]*sms="(\d+).(\d+):(\d+):(\d+)" [^>]*/><pr [^>]*b="(\d+)" ', data)
        for match in matches:
            self._fytion.contacts[match[0]] = {
                'user_id': match[0], 
                'uri': match[3], 
                'name': match[6], 
                'phone': match[4],
                'fetion_id': match[2],
                'sms': {'day': int(match[7]), 'h': int(match[8]), 'm': int(match[9]), 's': int(match[10])},
                'presence': match[11]
            }
            #log('update presence: %s(%s)' % (match[0], match[6]))

class subscribe(Processor):

    def __init__(self, fytion, sequencer):
        super(subscribe,self).__init__(fytion, sequencer)
        # 0 init, 1 sent
        self._state = 0
        sequencer.register_client(self._I, self)
        
    def sub(self):
        body = """<args><subscription self="v4default;mail-count;impresa;sms-online-status;feed-version;feed-type;es2all" buddy="v4default;feed-version;feed-type;es2all" version="" /></args>"""
    
        self._Q += 1
        headers = self.get_headers('SUB')
        headers.append(('N', 'PresenceV4'))
        c = self.get_request('SUB', self._fytion.Domain, self._fytion.SIPCVer, headers, body)
        
        self._state = 1
        self._sequencer.dispatch(c)
        
    def recv_pkt(self, data):
        if self._state == 0:
            return 1
        if data['status'] == 200:
            pass
        else:
            log("Sub failed: %d %s" %(data['status'], data['text']))
        
        self._sequencer.deregister_client(self._I, self)
        return 0

class message_recv(Processor):

    def __init__(self, fytion, sequencer, I):
        super(message_recv,self).__init__(fytion, sequencer, I)
        sequencer.register_service(self._I, self)
        
    def recv_pkt(self, data):
        if data['method'] != 'M':
            return 1
        
        headers = [('I', self._I), ('Q', data['headers']['Q']), ('F', data['headers']['F'])]
        c = self.get_response(200, 'OK', self._fytion.SIPCVer, headers)
        
        self._sequencer.dispatch(c)
        self._fytion.get_message({
            'from': data['headers']['F'],
            'content-type': data['headers']['C'],
            'body': data['body'],
            'timestamp': data['headers']['D']
        })

        self._sequencer.deregister_service(self._I, self)
        return 0

class message_send(Processor):

    def __init__(self, fytion, sequencer):
        super(message_send,self).__init__(fytion, sequencer)
        # 0 init, 1 sent
        self._state = 0
        sequencer.register_client(self._I, self)
        
    def send(self, msg, to='', sms=False):
        self._Q += 1
        headers = self.get_headers('M')
        if to:
            headers.append(('T', to))
        if sms:
            headers.append(('N', 'SendCatSMS'))
        else:
            headers.append(('C', 'text/plain'))
            headers.append(('K', 'SaveHistory'))
            headers.append(('N', 'CatMsg'))
        c = self.get_request('M', self._fytion.Domain, self._fytion.SIPCVer, headers, msg)
        
        self._state = 1
        self._sequencer.dispatch(c)
        
    def recv_pkt(self, data):
        if self._state == 0:
            return 1
        if data['status'] == 200:
            log("Msg sent")
        elif data['status'] == 280:
            log("Msg sent as sms")
        else:
            log("Send fail: %d %s" %(data['status'], data['text']))
        
        self._sequencer.deregister_client(self._I, self)
        return 0

class keepbusy(Processor):

    def __init__(self, fytion, sequencer):
        super(keepbusy,self).__init__(fytion, sequencer)
        # 0 init, 1 sent
        self._state = 0
        sequencer.register_client(self._I, self)
        
    def keepbusy(self):
        self._Q += 1
        headers = self.get_headers('O')
        headers.append(('N', 'KeepConnectionBusy'))
        c = self.get_request('O', self._fytion.Domain, self._fytion.SIPCVer, headers)
        
        self._state = 1
        self._sequencer.dispatch(c)
        
    def recv_pkt(self, data):
        if self._state == 0:
            return 1
        if data['status'] == 444 or data['status'] == 200:
            pass
        else:
            log("Keepbusy: %d %s" %(data['status'], data['text']))
        
        self._sequencer.deregister_client(self._I, self)
        return 0

class bye(Processor):

    def __init__(self, fytion, sequencer):
        super(bye,self).__init__(fytion, sequencer)
        # 0 init, 1 sent
        self._state = 0
        sequencer.register_client(self._I, self)
        
    def bye(self):
        self._Q += 1
        headers = self.get_headers('B')
        c = self.get_request('B', self._fytion.Domain, self._fytion.SIPCVer, headers)
        
        self._state = 1
        self._sequencer.dispatch(c)
        
    def recv_pkt(self, data):
        if self._state == 0:
            return 1

        self._sequencer.deregister_client(self._I, self)
        if data['status'] == 200:
            pass
        else:
            log("Bye: %d %s" %(data['status'], data['text']))
        # close connection anyway
        self._sequencer.close()
        
        return 0

class invited(Processor):

    def __init__(self, fytion, sequencer, I):
        super(invited,self).__init__(fytion, sequencer, I)
        sequencer.register_service(self._I, self)
        
    def recv_pkt(self, data):
        if data['method'] != 'I':
            return 1
        
        headers = [('I', self._I), ('Q', data['headers']['Q']), ('F', data['headers']['F'])]
        c = self.get_response(200, 'OK', self._fytion.SIPCVer, headers)
        
        self._sequencer.dispatch(c)
        
        proxy = re.search(r'address="([0-9.:]+);', data['headers']['A']).group(1)
        credential = re.search(r'credential="([0-9.]+)"', data['headers']['A']).group(1)
        
        self._fytion.setup_session({
            'proxy': proxy,
            'credential': credential,
            'from_uri': data['headers']['F']
        })

        self._sequencer.deregister_service(self._I, self)
        return 0

class start_chat(Processor):

    def __init__(self, fytion, sequencer):
        super(start_chat,self).__init__(fytion, sequencer)
        # 0 init, 1 sent
        self._state = 0
        sequencer.register_client(self._I, self)
        
    def start(self, uri):
        self._Q += 1
        headers = self.get_headers('S')
        headers.append(('N', 'StartChat'))
        c = self.get_request('S', self._fytion.Domain, self._fytion.SIPCVer, headers)
        
        self._state = 1
        self._to_uri = uri
        self._sequencer.dispatch(c)
        
    def recv_pkt(self, data):
        if self._state == 0:
            return 1

        if data['status'] == 200:
            proxy = re.search(r'address="([0-9.:]+);', data['headers']['A']).group(1)
            credential = re.search(r'credential="([0-9.]+)"', data['headers']['A']).group(1)
            log("Start chat")
            self._fytion.setup_session({
                'proxy': proxy,
                'credential': credential,
                'from_uri': '',
                'to_uri': self._to_uri
            })
        else:
            log("Cannot start chat: %d %s" %(data['status'], data['text']))
        
        self._sequencer.deregister_client(self._I, self)
        return 0

class invite(Processor):

    def __init__(self, fytion, sequencer):
        super(invite,self).__init__(fytion, sequencer)
        # 0 init, 1 sent
        self._state = 0
        sequencer.register_client(self._I, self)
        
    def invite(self, uri):
        body = '''<args><contacts><contact uri="%s" /></contacts></args>'''
        body = body % uri
        self._Q += 1
        headers = self.get_headers('S')
        headers.append(('N', 'InviteBuddy'))
        c = self.get_request('S', self._fytion.Domain, self._fytion.SIPCVer, headers, body)
        
        self._state = 1
        self._sequencer.dispatch(c)
        
    def recv_pkt(self, data):
        if self._state == 0:
            return 1

        if data['status'] == 200:
            log("Finish invite buddy")
            self._fytion.invite_fin()
        else:
            log("Cannot invite buddy: %d %s" %(data['status'], data['text']))
        
        self._sequencer.deregister_client(self._I, self)
        return 0
